unit billboards;

{*
Models are replaces with billboards when model is too far away. Usually trees or so.
}

interface

uses Classes, SysUtils, gl, glu, glut, textures, opengls, debugs, vumaths, grounds, vectors, glSOARMain, Math;

type
  TBillboard = class
    Texture: TTexture;
    Position: TVector3;
    Rotation: TVector3;
    RotationFixed : TVector3; 
    Size: TVector2;
    Hue: single;

    constructor Create(ATextureFileName: string); virtual;
    destructor Destroy; override;
    procedure Show(AFixed : boolean = false);
    function Clone : TBillboard;
  end;
  
  TBillboardList = class(TList)
    LastTime : TDateTime;
    constructor Create; virtual;
    procedure SortBy(AX, AY : integer; AUpdateIntervalMs : integer);
    procedure Randomize(ACount, AMinX, AMinY, AMaxX, AMaxY : integer; ATextures : array of string; AWidth, AHeight : integer);
    function ReSquare(AOldX, AOldY, ANewX, ANewY, ASide, AThreshold : real; AInitial : boolean) : boolean;
  end;

implementation

constructor TBillboard.Create(ATextureFileName: string);
begin
  if ATextureFileName <> '' then
    Texture := TextureList.Load(ATextureFileName);
  Size.X := 30;
  Size.Y := 30;
end;

function TBillboard.Clone : TBillboard;
begin
  result := TBillboard.Create(Texture.FileName);
  result.Size.X := Size.X; 
  result.Size.Y := Size.Y;
  result.Position := Position; 
  result.Rotation := Rotation; 
  result.RotationFixed := RotationFixed;
  result.Hue := Hue; 
end;

destructor TBillboard.Destroy;
begin
  Texture.Free;
  inherited;
end;

procedure TBillboard.Show(AFixed : boolean);
begin
  // calculate rotation depending on the eye position
  Rotation.X := 90;
  if AFixed then
    Rotation.Y := RotationFixed.Z
  else
    Rotation.Y := (180/pi)*( Atan2(Position.X - Ground.Position.X,Position.Y - Ground.Position.Y) ) - 90;
  // placement
  glTranslateF(Position.X, Position.Y, Position.Z);
  glRotatef(Rotation.X,1.0,0.0,0.0);
  glRotatef(Rotation.Y,0.0,1.0,0.0);
  glRotatef(Rotation.Z,0.0,0.0,1.0);
  // front
  glBindTexture(GL_TEXTURE_2D, Texture.Id);
  glBegin(GL_QUADS);
  glTexCoord2d(0.99, 0.01);  glVertex3f( Size.X/2, Size.Y, 0);
  glTexCoord2d(0.01, 0.01);  glVertex3f(-Size.X/2, Size.Y, 0);
  glTexCoord2d(0.01, 0.99);  glVertex3f(-Size.X/2, 0,      0);
  glTexCoord2d(0.99, 0.99);  glVertex3f( Size.X/2, 0,      0);
  glEnd;
end;

var GroundDistanceOrigin : TVector2;

function GroundDistance(Item1, Item2: Pointer): Integer;
{*
TListSortCompare function for inverse-distance sort of ground billboards, origin is in GroundDistanceOrigin
}
var d1,d2 : single;
begin
  // abs sorting is 15% faster but less accurate
  //d1 := abs(TBillboard(Item1).Position.X-GroundDistanceOrigin.X)+abs(TBillboard(Item1).Position.Y-GroundDistanceOrigin.Y);
  //d2 := abs(TBillboard(Item2).Position.X-GroundDistanceOrigin.X)+abs(TBillboard(Item2).Position.Y-GroundDistanceOrigin.Y);
  // sqr sorting is 15% slower but more accurate (recomended) 
  d1 := sqr(TBillboard(Item1).Position.X-GroundDistanceOrigin.X)+sqr(TBillboard(Item1).Position.Y-GroundDistanceOrigin.Y);
  d2 := sqr(TBillboard(Item2).Position.X-GroundDistanceOrigin.X)+sqr(TBillboard(Item2).Position.Y-GroundDistanceOrigin.Y);
  if d1 = d2 then
    result := 0
  else begin
    if d1 < d2 then
      result := 1
    else
      result := -1;
  end;
end;

{ TBillboardList }

constructor TBillboardList.Create;
{*
Initialize timing
}
begin
  inherited;
  LastTime := now;
end;

procedure TBillboardList.SortBy(AX, AY : integer; AUpdateIntervalMs : integer);
{*
Sort billboards by distance
}
begin
  if (now - LastTime)*86400000 < AUpdateIntervalMs then
    exit;
  //writeln((now - LastTime)*86400000:1:12);
  GroundDistanceOrigin.X := AX;
  GroundDistanceOrigin.Y := AY;
  Sort(GroundDistance);
  LastTime := now;
end;

procedure TBillboardList.Randomize(ACount, AMinX, AMinY, AMaxX, AMaxY : integer; ATextures : array of string; AWidth, AHeight : integer);
{*
Create given amount of billboards on random positions and rotations
}
var i,t : integer;
    b : TBillboard;
begin
  for i := 0 to ACount do
  begin
    t := Random(High(ATextures)+1);
    b := TBillboard.Create(ATextures[t]);
    b.Size.X := AWidth;
    b.Size.Y := AHeight;
    b.Position.X := AMinX+Random(AMaxX-AMinX); 
    b.Position.Y := AMinY+Random(AMaxY-AMinY);
    b.Position.Z := GetTerrainZ(b.Position.X,b.Position.Y,b.Position.Z);
    b.RotationFixed.Z := Random(360);
    b.Hue := 1-random(20)/100;
    Add(b);
  end;
end;

function TBillboardList.ReSquare(AOldX, AOldY, ANewX, ANewY, ASide, AThreshold : real; AInitial : boolean) : boolean;
{*
Randomize bilboards when moved from old position A to new position B,
all bilboards too far (TF) are randomly placed to new uncovered area (UA)
It is used for nearby grass rendering
       size
 +------------+
 |TF TF TF TF | <-- threshold
 |TF +------------+
 |TF |(A)     : UA|
 |TF |        : UA|
 |TF |    (B) : UA|
 +---|........+ UA|
     |UA UA UA UA |
     +------------+
Return true if bilboards was resquared, false if new-old < threshold
}
var i,k : integer;
    a,b,size2 : real;
    bz : boolean;
begin
  // check threshold
  k := 100;
  size2 := ASide / 2;
  result := (abs(AOldX-ANewX) >= AThreshold)
          or(abs(AOldY-ANewY) >= AThreshold)
          or(AInitial);
  if not result then
    exit;
  // randomize too far bilboards to newly uncovered area
  for i := 0 to Count-1 do
    with TBillboard(Items[i]) do
    begin
      // initial randomization
      if AInitial then
      begin
        Position.X := ANewX+random(round(size2*k))/k-random(round(size2*k))/k;
        Position.Y := ANewY+random(round(size2*k))/k-random(round(size2*k))/k;
        Position.Z := GetTerrainZ(Position.X,Position.Y,Position.Z);
        Continue;
      end;
      bz := false;
      // x
      if Position.X - ANewX <= -size2 then
      begin
        a := max(min(AOldX+size2,ANewX+size2),ANewX-size2);
        b := max(AOldX+size2,ANewX+size2);
        Position.X := a+random(round(b*k)-round(a*k)+1) / k;
        bz := true;
      end;
      if Position.X - ANewX >= +size2 then
      begin
        a := min(AOldX-size2,ANewX-size2);
        b := min(max(AOldX-size2,ANewX-size2),ANewX+size2);
        Position.X := a+random(round(b*k)-round(a*k)+1) / k;
        bz := true;
      end;
      // y
      if Position.Y - ANewY <= -size2 then
      begin
        a := max(min(AOldY+size2,ANewY+size2),ANewY-size2);
        b := max(AOldY+size2,ANewY+size2);
        Position.Y := a+random(round(b*k)-round(a*k)+1) / k; // random use int, if k=100, then *100/100 makes it real in range of 0.01-1.00 which makes grass placement of the integer grid
        bz := true;
      end;
      if Position.Y - ANewY >= +size2 then
      begin
        a := min(AOldY-size2,ANewY-size2);
        b := min(max(AOldY-size2,ANewY-size2),ANewY+size2);
        Position.Y := a+random(round(b*k)-round(a*k)+1) / k;
        bz := true;
      end;
      if bz then
        Position.Z := GetTerrainZ(Position.X,Position.Y,Position.Z);
    end;
end;

initialization

finalization

end.
